/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Alexey V. Varlamov
 */

package org.apache.harmony.security.fortress;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Permission;
import java.security.Principal;
import java.security.UnresolvedPermission;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.harmony.security.DefaultPolicyScanner;
import org.apache.harmony.security.DefaultPolicyScanner.GrantEntry;
import org.apache.harmony.security.DefaultPolicyScanner.KeystoreEntry;
import org.apache.harmony.security.DefaultPolicyScanner.PermissionEntry;
import org.apache.harmony.security.DefaultPolicyScanner.PrincipalEntry;
import org.apache.harmony.security.PolicyEntry;
import org.apache.harmony.security.UnresolvedPrincipal;
import org.apache.harmony.security.fortress.PolicyUtils.ExpansionFailedException;
import org.apache.harmony.security.internal.nls.Messages;

/**
 * This is a basic loader of policy files. It delegates lexical analysis to a
 * pluggable scanner and converts received tokens to a set of
 * {@link org.apache.harmony.security.PolicyEntry PolicyEntries}. For details of
 * policy format, see the {@link org.apache.harmony.security.DefaultPolicy
 * default policy description}. <br>
 * For ordinary uses, this class has just one public method <code>parse()</code>
 * , which performs the main task. Extensions of this parser may redefine
 * specific operations separately, by overriding corresponding protected
 * methods. <br>
 * This implementation is effectively thread-safe, as it has no field references
 * to data being processed (that is, passes all the data as method parameters).
 * 
 * @see org.apache.harmony.security.DefaultPolicy
 * @see org.apache.harmony.security.DefaultPolicyScanner
 * @see org.apache.harmony.security.PolicyEntry
 */
public class DefaultPolicyParser {

	/**
	 * Specific handler for expanding <i>self</i> and <i>alias</i> protocols.
	 */
	class PermissionExpander implements PolicyUtils.GeneralExpansionHandler {

		// Store KeyStore
		private KeyStore ks;

		// Store GrantEntry
		private DefaultPolicyScanner.GrantEntry ge;

		/**
		 * Combined setter of all required fields.
		 */
		public PermissionExpander configure(DefaultPolicyScanner.GrantEntry ge,
				KeyStore ks) {
			this.ge = ge;
			this.ks = ks;
			return this;
		}

		// Formats a string describing the passed Principal.
		private String pc2str(Principal pc) {
			final String klass = pc.getClass().getName();
			final String name = pc.getName();
			final StringBuilder sb = new StringBuilder(klass.length()
					+ name.length() + 5);
			return sb.append(klass).append(" \"").append(name).append("\"") //$NON-NLS-1$ //$NON-NLS-2$
					.toString();
		}

		/**
		 * Resolves the following protocols:
		 * <dl>
		 * <dt>self
		 * <dd>Denotes substitution to a principal information of the parental
		 * GrantEntry. Returns a space-separated list of resolved Principals
		 * (including wildcarded), formatting each as <b>class
		 * &quot;name&quot;</b>. If parental GrantEntry has no Principals,
		 * throws ExpansionFailedException.
		 * <dt>alias:<i>name</i>
		 * <dd>Denotes substitution of a KeyStore alias. Namely, if a KeyStore
		 * has an X.509 certificate associated with the specified name, then
		 * returns <b>org.apache.harmony.javax.security.auth.x500.X500Principal
		 * &quot;<i>DN</i>&quot;</b> string, where <i>DN</i> is a certificate's
		 * subject distinguished name.
		 * </dl>
		 * 
		 * @throws ExpansionFailedException
		 *             - if protocol is other than <i>self</i> or <i>alias</i>,
		 *             or if data resolution failed
		 */
		@Override
		public String resolve(String protocol, String data)
				throws PolicyUtils.ExpansionFailedException {

			if ("self".equals(protocol)) { //$NON-NLS-1$
				// need expanding to list of principals in grant clause
				if (ge.principals != null && ge.principals.size() != 0) {
					final StringBuilder sb = new StringBuilder();
					for (final PrincipalEntry pr : ge.principals) {
						if (pr.klass == null) {
							// aliased X500Principal
							try {
								sb.append(pc2str(getPrincipalByAlias(ks,
										pr.name)));
							} catch (final Exception e) {
								throw new PolicyUtils.ExpansionFailedException(
										Messages.getString(
												"security.143", pr.name), e); //$NON-NLS-1$
							}
						} else {
							sb.append(pr.klass).append(" \"").append(pr.name) //$NON-NLS-1$
									.append("\" "); //$NON-NLS-1$
						}
					}
					return sb.toString();
				} else {
					throw new PolicyUtils.ExpansionFailedException(
							Messages.getString("security.144")); //$NON-NLS-1$
				}
			}
			if ("alias".equals(protocol)) { //$NON-NLS-1$
				try {
					return pc2str(getPrincipalByAlias(ks, data));
				} catch (final Exception e) {
					throw new PolicyUtils.ExpansionFailedException(
							Messages.getString("security.143", data), e); //$NON-NLS-1$
				}
			}
			throw new PolicyUtils.ExpansionFailedException(Messages.getString(
					"security.145", protocol)); //$NON-NLS-1$
		}
	}

	// Pluggable scanner for a specific file format
	private final DefaultPolicyScanner scanner;

	/**
	 * Default constructor,
	 * {@link org.apache.harmony.security.DefaultPolicyScanner
	 * DefaultPolicyScanner} is used.
	 */
	public DefaultPolicyParser() {
		scanner = new DefaultPolicyScanner();
	}

	/**
	 * Extension constructor for plugging-in custom scanner.
	 */
	public DefaultPolicyParser(DefaultPolicyScanner s) {
		scanner = s;
	}

	/**
	 * Returns a subject's X500Principal of an X509Certificate, which is
	 * associated with the specified keystore alias.
	 * 
	 * @param ks
	 *            KeyStore for resolving Certificate, may be <code>null</code>
	 * @param alias
	 *            alias to a certificate
	 * @return X500Principal with a subject distinguished name
	 * @throws KeyStoreException
	 *             if KeyStore is <code>null</code> or if it failed to provide a
	 *             certificate
	 * @throws CertificateException
	 *             if found certificate is not an X509Certificate
	 */
	protected Principal getPrincipalByAlias(KeyStore ks, String alias)
			throws KeyStoreException, CertificateException {

		if (ks == null) {
			throw new KeyStoreException(Messages.getString(
					"security.147", alias)); //$NON-NLS-1$
		}
		// XXX cache found certs ??
		final Certificate x509 = ks.getCertificate(alias);
		if (x509 instanceof X509Certificate) {
			return ((X509Certificate) x509).getSubjectX500Principal();
		} else {
			throw new CertificateException(Messages.getString("security.148", //$NON-NLS-1$
					alias, x509));
		}
	}

	/**
	 * Returns the first successfully loaded KeyStore, from the specified list
	 * of possible locations. This method iterates over the list of
	 * KeystoreEntries; for each entry expands <code>url</code> and
	 * <code>type</code>, tries to construct instances of specified URL and
	 * KeyStore and to load the keystore. If it is loaded, returns the keystore,
	 * otherwise proceeds to the next KeystoreEntry. <br>
	 * <b>Note:</b> an url may be relative to the policy file location or
	 * absolute.
	 * 
	 * @param keystores
	 *            list of available KeystoreEntries
	 * @param base
	 *            the policy file location
	 * @param system
	 *            system properties, used for property expansion
	 * @param resolve
	 *            flag enabling/disabling property expansion
	 * @return the first successfully loaded KeyStore or <code>null</code>
	 */
	protected KeyStore initKeyStore(List<KeystoreEntry> keystores, URL base,
			Properties system, boolean resolve) {

		for (int i = 0; i < keystores.size(); i++) {
			try {
				final DefaultPolicyScanner.KeystoreEntry ke = keystores.get(i);
				if (resolve) {
					ke.url = PolicyUtils.expandURL(ke.url, system);
					if (ke.type != null) {
						ke.type = PolicyUtils.expand(ke.type, system);
					}
				}
				if (ke.type == null || ke.type.length() == 0) {
					ke.type = KeyStore.getDefaultType();
				}
				final KeyStore ks = KeyStore.getInstance(ke.type);
				final URL location = new URL(base, ke.url);
				final InputStream is = AccessController
						.doPrivileged(new PolicyUtils.URLLoader(location));
				try {
					ks.load(is, null);
				} finally {
					is.close();
				}
				return ks;
			} catch (final Exception e) {
				// TODO: log warning
			}
		}
		return null;
	}

	/**
	 * This is the main business method. It manages loading process as follows:
	 * the associated scanner is used to parse the stream to a set of
	 * {@link org.apache.harmony.security.DefaultPolicyScanner.GrantEntry
	 * composite tokens}, then this set is iterated and each token is translated
	 * to a PolicyEntry. Semantically invalid tokens are ignored, the same as
	 * void PolicyEntries. <br>
	 * A policy file may refer to some KeyStore(s), and in this case the first
	 * valid reference is initialized and used in processing tokens.
	 * 
	 * @param location
	 *            an URL of a policy file to be loaded
	 * @param system
	 *            system properties, used for property expansion
	 * @return a collection of PolicyEntry objects, may be empty
	 * @throws Exception
	 *             IO error while reading location or file syntax error
	 */
	public Collection<PolicyEntry> parse(URL location, Properties system)
			throws Exception {

		final boolean resolve = PolicyUtils.canExpandProperties();
		final Reader r = new BufferedReader(new InputStreamReader(
				AccessController.doPrivileged(new PolicyUtils.URLLoader(
						location))));

		final Collection<GrantEntry> grantEntries = new HashSet<GrantEntry>();
		final List<KeystoreEntry> keystores = new ArrayList<KeystoreEntry>();

		try {
			scanner.scanStream(r, grantEntries, keystores);
		} finally {
			r.close();
		}

		// XXX KeyStore could be loaded lazily...
		final KeyStore ks = initKeyStore(keystores, location, system, resolve);

		final Collection<PolicyEntry> result = new HashSet<PolicyEntry>();
		for (final GrantEntry ge : grantEntries) {
			try {
				final PolicyEntry pe = resolveGrant(ge, ks, system, resolve);
				if (!pe.isVoid()) {
					result.add(pe);
				}
			} catch (final Exception e) {
				// TODO: log warning
			}
		}

		return result;
	}

	/**
	 * Translates GrantEntry token to PolicyEntry object. It goes step by step,
	 * trying to resolve each component of the GrantEntry:
	 * <ul>
	 * <li>If <code>codebase</code> is specified, expand it and construct an
	 * URL.
	 * <li>If <code>signers</code> is specified, expand it and obtain
	 * corresponding Certificates.
	 * <li>If <code>principals</code> collection is specified, iterate over it.
	 * For each PrincipalEntry, expand name and if no class specified, resolve
	 * actual X500Principal from a KeyStore certificate; otherwise keep it as
	 * UnresolvedPrincipal.
	 * <li>Iterate over <code>permissions</code> collection. For each
	 * PermissionEntry, try to resolve (see method
	 * {@link #resolvePermission(DefaultPolicyScanner.PermissionEntry, DefaultPolicyScanner.GrantEntry, KeyStore, Properties, boolean)
	 * resolvePermission()}) a corresponding permission. If resolution failed,
	 * ignore the PermissionEntry.
	 * </ul>
	 * In fact, property expansion in the steps above is conditional and is
	 * ruled by the parameter <i>resolve</i>. <br>
	 * Finally a new PolicyEntry is created, which associates the trinity of
	 * resolved URL, Certificates and Principals to a set of granted
	 * Permissions.
	 * 
	 * @param ge
	 *            GrantEntry token to be resolved
	 * @param ks
	 *            KeyStore for resolving Certificates, may be <code>null</code>
	 * @param system
	 *            system properties, used for property expansion
	 * @param resolve
	 *            flag enabling/disabling property expansion
	 * @return resolved PolicyEntry
	 * @throws Exception
	 *             if unable to resolve codebase, signers or principals of the
	 *             GrantEntry
	 * @see DefaultPolicyScanner.PrincipalEntry
	 * @see DefaultPolicyScanner.PermissionEntry
	 * @see org.apache.harmony.security.PolicyUtils
	 */
	protected PolicyEntry resolveGrant(DefaultPolicyScanner.GrantEntry ge,
			KeyStore ks, Properties system, boolean resolve) throws Exception {

		URL codebase = null;
		Certificate[] signers = null;
		final Set<Principal> principals = new HashSet<Principal>();
		final Set<Permission> permissions = new HashSet<Permission>();
		if (ge.codebase != null) {
			codebase = new URL(resolve ? PolicyUtils.expandURL(ge.codebase,
					system) : ge.codebase);
		}
		if (ge.signers != null) {
			if (resolve) {
				ge.signers = PolicyUtils.expand(ge.signers, system);
			}
			signers = resolveSigners(ks, ge.signers);
		}
		if (ge.principals != null) {
			for (final PrincipalEntry pe : ge.principals) {
				if (resolve) {
					pe.name = PolicyUtils.expand(pe.name, system);
				}
				if (pe.klass == null) {
					principals.add(getPrincipalByAlias(ks, pe.name));
				} else {
					principals.add(new UnresolvedPrincipal(pe.klass, pe.name));
				}
			}
		}
		if (ge.permissions != null) {
			for (final PermissionEntry pe : ge.permissions) {
				try {
					permissions.add(resolvePermission(pe, ge, ks, system,
							resolve));
				} catch (final Exception e) {
					// TODO: log warning
				}
			}
		}
		return new PolicyEntry(new CodeSource(codebase, signers), principals,
				permissions);
	}

	/**
	 * Translates PermissionEntry token to Permission object. First, it performs
	 * general expansion for non-null <code>name</code> and properties expansion
	 * for non-null <code>name</code>, <code>action</code> and
	 * <code>signers</code>. Then, it obtains signing Certificates(if any),
	 * tries to find a class specified by <code>klass</code> name and
	 * instantiate a corresponding permission object. If class is not found or
	 * it is signed improperly, returns UnresolvedPermission.
	 * 
	 * @param pe
	 *            PermissionEntry token to be resolved
	 * @param ge
	 *            parental GrantEntry of the PermissionEntry
	 * @param ks
	 *            KeyStore for resolving Certificates, may be <code>null</code>
	 * @param system
	 *            system properties, used for property expansion
	 * @param resolve
	 *            flag enabling/disabling property expansion
	 * @return resolved Permission object, either of concrete class or
	 *         UnresolvedPermission
	 * @throws Exception
	 *             if failed to expand properties, or to get a Certificate, or
	 *             to create an instance of a successfully found class
	 */
	protected Permission resolvePermission(
			DefaultPolicyScanner.PermissionEntry pe,
			DefaultPolicyScanner.GrantEntry ge, KeyStore ks, Properties system,
			boolean resolve) throws Exception {
		if (pe.name != null) {
			pe.name = PolicyUtils.expandGeneral(pe.name,
					new PermissionExpander().configure(ge, ks));
		}
		if (resolve) {
			if (pe.name != null) {
				pe.name = PolicyUtils.expand(pe.name, system);
			}
			if (pe.actions != null) {
				pe.actions = PolicyUtils.expand(pe.actions, system);
			}
			if (pe.signers != null) {
				pe.signers = PolicyUtils.expand(pe.signers, system);
			}
		}
		final Certificate[] signers = (pe.signers == null) ? null
				: resolveSigners(ks, pe.signers);
		try {
			final Class<?> klass = Class.forName(pe.klass);
			if (PolicyUtils.matchSubset(signers, klass.getSigners())) {
				return PolicyUtils.instantiatePermission(klass, pe.name,
						pe.actions);
			}
		} catch (final ClassNotFoundException cnfe) {
		}
		// maybe properly signed class will be loaded later
		return new UnresolvedPermission(pe.klass, pe.name, pe.actions, signers);
	}

	/**
	 * Takes a comma-separated list of aliases and obtains corresponding
	 * certificates.
	 * 
	 * @param ks
	 *            KeyStore for resolving Certificates, may be <code>null</code>
	 * @param signers
	 *            comma-separated list of certificate aliases, must be not
	 *            <code>null</code>
	 * @return an array of signing Certificates
	 * @throws Exception
	 *             if KeyStore is <code>null</code> or if it failed to provide a
	 *             certificate
	 */
	protected Certificate[] resolveSigners(KeyStore ks, String signers)
			throws Exception {
		if (ks == null) {
			throw new KeyStoreException(Messages.getString("security.146", //$NON-NLS-1$
					signers));
		}

		final Collection<Certificate> certs = new HashSet<Certificate>();
		final StringTokenizer snt = new StringTokenizer(signers, ","); //$NON-NLS-1$
		while (snt.hasMoreTokens()) {
			// XXX cache found certs ??
			certs.add(ks.getCertificate(snt.nextToken().trim()));
		}
		return certs.toArray(new Certificate[certs.size()]);
	}
}
